/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.apache.cgiservlet;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * The base class for CGI script servlets
 * 
 * @author Sergey I. Salishev
 */
public abstract class CGIServlet extends HttpServlet {

	private static final long serialVersionUID = 1L;

	private static final int BUFFER_SIZE = 8192;

	private final ExecutorService executor = Executors.newCachedThreadPool();
	
	private Process runScript(final String[] args, Map<String, String> env)
			throws Exception {
		
		ContinuedFuture<Process> task = createProcessTask(args, env);
		executor.execute(task);
		return task.get();
	}

	public void service(HttpServletRequest req, HttpServletResponse res)
			throws ServletException, IOException {

		final String method = req.getMethod().toLowerCase();
		if (!"get".equals(method) && !"post".equals(method)) {
			res.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
			return;
		}

		final String scriptPath = getServletContext().getRealPath(
				req.getServletPath());
		
		if (!new File(scriptPath).exists()) {
			res.sendError(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
		dispatch(scriptPath, req, res);
	}

	private void dispatch(String path, HttpServletRequest req,
			HttpServletResponse res) throws IOException {
		
		try {
			Process p = runScript(getArgs(path, req), getEnv(req));
			
			Future<Void> postTask = null;
			if (req.getMethod().equalsIgnoreCase("post")) {
				postTask = postContentAsync(req, p);
			}
			processOutput(res, p.getInputStream());
			
			if (postTask != null && !postTask.isDone()) {
				postTask.cancel(true);
			}
		} catch (Exception e) {
			e.printStackTrace();
			res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
		}
	}

	private Future<Void> postContentAsync(final HttpServletRequest req, final Process p) {
		return executor.submit(new Callable<Void>() {

			@Override
			public Void call() throws Exception {
				postContent(req, p.getOutputStream());
				return null;
			}
			
		});
	}

	private void processOutput(HttpServletResponse res, InputStream inStream)
			throws IOException {
		BufferedReader in = new BufferedReader(new InputStreamReader(inStream));
		Writer resOut = res.getWriter();
		processHeaders(res, in);
		copyRemaining(in, resOut);
		in.close();
		resOut.close();
	}

	private void copyRemaining(Reader in, Writer out) throws IOException {
		char[] buf = new char[BUFFER_SIZE];
		while (true) {
			int read = in.read(buf);
			if (read == -1) {
				break;
			}
			out.write(buf, 0, read);
		}
	}

	private void processHeaders(HttpServletResponse res, BufferedReader in)
			throws IOException {

		boolean firstLine = true;
		while (true) {
			String line = in.readLine();
			if (line == null) {
				break;
			}
			line = line.trim();
			if (line.equals("")) {
				break;
			}
			int colon = line.indexOf(":");
			if (colon == -1) {
				if (firstLine) {
					parseStatus(res, line);
				}
			} else {
				parseHeader(res, line, colon);
			}
			firstLine = false;
		}
	}

	private void parseHeader(HttpServletResponse res, String line, int colon) {
		String name = line.substring(0, colon);
		String value = line.substring(colon + 1).trim();
		if (name.equalsIgnoreCase("Status")) {
			String code = value.substring(0, value.indexOf(' '));
			res.setStatus(Integer.parseInt(code));
		} else if (name.equalsIgnoreCase("Content-type")) {
			res.setContentType(value);
		} else if (name.equalsIgnoreCase("Content-length")) {
			res.setContentLength(Integer.parseInt(value));
		} else if (name.equalsIgnoreCase("Location")) {
			res.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
			res.setHeader(name, value);
		} else {
			res.setHeader(name, value);
		}
	}

	private void parseStatus(HttpServletResponse res, String line) {
		String[] words = line.split("\\s");
		res.setStatus(Integer.parseInt(words[1]));
	}

	private void postContent(HttpServletRequest req, OutputStream outStream)
			throws IOException {
		char[] buf = new char[BUFFER_SIZE];
		Writer out = new OutputStreamWriter(outStream);
		Reader reqIn = req.getReader();
		for (int length = 0; length < req.getContentLength();) {
			int read = reqIn.read(buf);
			if (read == -1) {
				break;
			}
			out.write(buf, 0, read);
			length += read;
		}
		out.close();
	}

	private String[] getArgs(String path, HttpServletRequest req) {
		String queryString = req.getQueryString();
		String argsString = path
				+ (queryString != null && !queryString.contains("=") ? "+"
						+ queryString : "");
		String argList[] = argsString.split("\\+");
		return argList;
	}

	private Map<String, String> getEnv(HttpServletRequest req) {
		Map<String, String> env = new HashMap<String, String>();

		putSpecific(req, env);
		
		for (Enumeration<?> hnEnum = req.getHeaderNames(); hnEnum
				.hasMoreElements();) {
			String name = (String) hnEnum.nextElement();
			String value = req.getHeader(name);
			putOptional(env, "HTTP_" + name.toUpperCase().replace('-', '_'),
					value);
		}
		return env;
	}

	private void putSpecific(HttpServletRequest req, Map<String, String> env) {
		putRequired(env, "PATH", "/usr/local/bin:/usr/ucb:/bin:/usr/bin");
		putRequired(env, "GATEWAY_INTERFACE", "CGI/1.1");
		putRequired(env, "SERVER_SOFTWARE", getServletContext().getServerInfo());
		putRequired(env, "SERVER_NAME", req.getServerName());
		putRequired(env, "SERVER_PORT", Integer.toString(req.getServerPort()));
		putRequired(env, "REMOTE_ADDR", req.getRemoteAddr());
		putRequired(env, "REMOTE_HOST", req.getRemoteHost());
		putRequired(env, "REQUEST_METHOD", req.getMethod());
		putRequired(env, "SCRIPT_NAME", req.getServletPath());
		putRequired(env, "SERVER_PROTOCOL", req.getProtocol());
		putRequired(env, "DOCUMENT_ROOT", getServletContext().getRealPath(
				req.getContextPath()));

		if (req.getContentLength() != -1) {
			putRequired(env, "CONTENT_LENGTH", Integer.toString(req
					.getContentLength()));
		}
		
		putOptional(env, "CONTENT_TYPE", req.getContentType());		
		putOptional(env, "PATH_INFO", req.getPathInfo());
		putOptional(env, "PATH_TRANSLATED", req.getPathTranslated());
		putOptional(env, "QUERY_STRING", req.getQueryString());
		putOptional(env, "REMOTE_USER", req.getRemoteUser());
		putOptional(env, "AUTH_TYPE", req.getAuthType());
	}

	private ContinuedFuture<Process> createProcessTask(final String[] args, 
			final Map<String, String> env) {
		
		final PipedOutputStream pipeOut = new PipedOutputStream();
		final PipedInputStream pipeIn = new PipedInputStream();
		final ScriptProcess process = new ScriptProcess(pipeOut, pipeIn, null);
		final File scriptFile = new File(args[0]);
		
		return new ContinuedFuture<Process>() {	
			public void run() {
				try {
					InputStream in = new PipedInputStream(pipeOut);
					OutputStream out = new PipedOutputStream(pipeIn);
					prepareScript(args, env, in, out, scriptFile);					
					set(process);
					runScript();															
					out.close();
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}	
		};
	}

	/**
	 * Runs the prepared script on the same thread as
	 * {@link #prepareScript(String[], Map, InputStream, OutputStream, File)}
	 * 
	 * @throws Exception
	 */
	protected abstract void runScript() throws Exception;

	/**
	 * Prepares the script run for running on the current thread
	 * @param args command line arguments
	 * @param env environment variables
	 * @param in input stream
	 * @param out output stream
	 * @param scriptFile script file
	 * @throws Exception
	 */
	protected abstract void prepareScript(String[] args,
			Map<String, String> env, InputStream in,
			OutputStream out, File scriptFile) throws Exception;

	private static void putRequired(Map<String, String> env, String key, 
			String value) {
		
		if (value == null || value.length() == 0) {
			throw new RuntimeException(key + " is required");
		}
		env.put(key, value);
	}

	private static void putOptional(Map<String, String> env, String key, 
			String value) {
		
		if (value != null && value.length() != 0) {
			env.put(key, value);
		}
	}
	
	@Override
	public void destroy() {
		executor.shutdown();
		super.destroy();
	}

}
